# -*- coding: utf-8 -*-
#
# AWL simulator - PLC core server messages
#
# Copyright 2013 Michael Buesch <m@bues.ch>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

from __future__ import division, absolute_import, print_function, unicode_literals
from awlsim.core.compat import *

from awlsim.coreserver.memarea import *
from awlsim.core.util import *
from awlsim.core.datatypehelpers import *
from awlsim.core.cpuspecs import *

import struct
import socket
import errno


class TransferError(Exception):
	pass

class AwlSimMessage(object):
	# Header format:
	#	Magic (16 bit)
	#	Message ID (16 bit)
	#	Sequence count (16 bit)
	#	Reserved (16 bit)
	#	Payload length (32 bit)
	#	Payload (optional)
	hdrStruct = struct.Struct(str(">HHHHI"))

	HDR_MAGIC		= 0x5710
	HDR_LENGTH		= hdrStruct.size

	EnumGen.start
	MSG_ID_REPLY		= EnumGen.item # Generic status reply
	MSG_ID_EXCEPTION	= EnumGen.item
	MSG_ID_PING		= EnumGen.item
	MSG_ID_PONG		= EnumGen.item
	MSG_ID_RESET		= EnumGen.item
	MSG_ID_RUNSTATE		= EnumGen.item
	MSG_ID_LOAD_SYMTAB	= EnumGen.item
	MSG_ID_LOAD_CODE	= EnumGen.item
	MSG_ID_LOAD_HW		= EnumGen.item
	MSG_ID_SET_OPT		= EnumGen.item
	MSG_ID_CPUDUMP		= EnumGen.item
	MSG_ID_MAINTREQ		= EnumGen.item
	MSG_ID_GET_CPUSPECS	= EnumGen.item
	MSG_ID_CPUSPECS		= EnumGen.item
	MSG_ID_REQ_MEMORY	= EnumGen.item
	MSG_ID_MEMORY		= EnumGen.item
	MSG_ID_INSNSTATE	= EnumGen.item
	EnumGen.end

	_strLenStruct = struct.Struct(str(">H"))

	@classmethod
	def packString(cls, string):
		try:
			data = string.encode("utf-8", "strict")
			return cls._strLenStruct.pack(len(data)) + data
		except (UnicodeError, struct.error) as e:
			raise ValueError

	@classmethod
	def unpackString(cls, data, offset = 0):
		try:
			(length, ) = cls._strLenStruct.unpack_from(data, offset)
			strBytes = data[offset + cls._strLenStruct.size :
					offset + cls._strLenStruct.size + length]
			if len(strBytes) != length:
				raise ValueError
			return (strBytes.decode("utf-8", "strict"),
				cls._strLenStruct.size + length)
		except (UnicodeError, struct.error) as e:
			raise ValueError

	def __init__(self, msgId, seq=0):
		self.msgId = msgId
		self.seq = seq

	def toBytes(self, payloadLength=0):
		return self.hdrStruct.pack(self.HDR_MAGIC,
					   self.msgId,
					   self.seq,
					   0,
					   payloadLength)

	@classmethod
	def fromBytes(cls, payload):
		return cls()

class AwlSimMessage_REPLY(AwlSimMessage):
	EnumGen.start
	STAT_OK		= EnumGen.item
	STAT_FAIL	= EnumGen.item
	EnumGen.end

	plStruct = struct.Struct(str(">HHH"))

	@classmethod
	def make(cls, inReplyToMsg, status):
		return cls(inReplyToMsg.msgId, inReplyToMsg.seq, status)

	def __init__(self, inReplyToId, inReplyToSeq, status):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_REPLY)
		self.inReplyToId = inReplyToId
		self.inReplyToSeq = inReplyToSeq
		self.status = status

	def toBytes(self):
		pl = self.plStruct.pack(self.inReplyToId,
					self.inReplyToSeq,
					self.status)
		return AwlSimMessage.toBytes(self, len(pl)) + pl

	@classmethod
	def fromBytes(cls, payload):
		try:
			inReplyToId, inReplyToSeq, status =\
				cls.plStruct.unpack(payload)
		except struct.error as e:
			raise TransferError("REPLY: Invalid data format")
		return cls(inReplyToId, inReplyToSeq, status)

class AwlSimMessage_PING(AwlSimMessage):
	def __init__(self):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_PING)

class AwlSimMessage_PONG(AwlSimMessage):
	def __init__(self):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_PONG)

class AwlSimMessage_RESET(AwlSimMessage):
	def __init__(self):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_RESET)

class AwlSimMessage_RUNSTATE(AwlSimMessage):
	EnumGen.start
	STATE_STOP	= EnumGen.item
	STATE_RUN	= EnumGen.item
	EnumGen.end

	plStruct = struct.Struct(str(">H"))

	def __init__(self, runState):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_RUNSTATE)
		self.runState = runState

	def toBytes(self):
		pl = self.plStruct.pack(self.runState)
		return AwlSimMessage.toBytes(self, len(pl)) + pl

	@classmethod
	def fromBytes(cls, payload):
		try:
			(runState, ) = cls.plStruct.unpack(payload)
		except struct.error as e:
			raise TransferError("RUNSTATE: Invalid data format")
		return cls(runState)

class AwlSimMessage_EXCEPTION(AwlSimMessage):
	def __init__(self, exceptionText):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_EXCEPTION)
		self.exceptionText = exceptionText

	def toBytes(self):
		try:
			textBytes = self.exceptionText.encode()
			return AwlSimMessage.toBytes(self, len(textBytes)) + textBytes
		except UnicodeError:
			raise TransferError("EXCEPTION: Unicode error")

	@classmethod
	def fromBytes(cls, payload):
		try:
			text = payload.decode()
		except UnicodeError:
			raise TransferError("EXCEPTION: Unicode error")
		return cls(text)

class AwlSimMessage_LOAD_SYMTAB(AwlSimMessage):
	def __init__(self, symTabText):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_LOAD_SYMTAB)
		self.symTabText = symTabText

	def toBytes(self):
		try:
			data = self.symTabText.encode()
			return AwlSimMessage.toBytes(self, len(data)) + data
		except UnicodeError:
			raise TransferError("LOAD_SYMTAB: Unicode error")

	@classmethod
	def fromBytes(cls, payload):
		try:
			symTabText = payload.decode()
		except UnicodeError:
			raise TransferError("LOAD_SYMTAB: Unicode error")
		return cls(symTabText)

class AwlSimMessage_LOAD_CODE(AwlSimMessage):
	def __init__(self, code):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_LOAD_CODE)
		self.code = code

	def toBytes(self):
		try:
			code = self.code.encode()
			return AwlSimMessage.toBytes(self, len(code)) + code
		except UnicodeError:
			raise TransferError("LOAD_CODE: Unicode error")

	@classmethod
	def fromBytes(cls, payload):
		try:
			code = payload.decode()
		except UnicodeError:
			raise TransferError("LOAD_CODE: Unicode error")
		return cls(code)

class AwlSimMessage_LOAD_HW(AwlSimMessage):
	def __init__(self, name, paramDict):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_LOAD_HW)
		self.name = name
		self.paramDict = paramDict

	def toBytes(self):
		payload = b""
		try:
			payload += self.packString(self.name)
			for pname, pval in self.paramDict.items():
				payload += self.packString(pname)
				payload += self.packString(pval)
			return AwlSimMessage.toBytes(self, len(payload)) + payload
		except (ValueError) as e:
			raise TransferError("LOAD_HW: Invalid data format")

	@classmethod
	def fromBytes(cls, payload):
		paramDict = {}
		offset = 0
		try:
			name, count = cls.unpackString(payload, offset)
			offset += count
			while offset < len(payload):
				pname, count = cls.unpackString(payload, offset)
				offset += count
				pval, count = cls.unpackString(payload, offset)
				offset += count
				paramDict[pname] = pval
		except (ValueError) as e:
			raise TransferError("LOAD_HW: Invalid data format")
		return cls(name = name, paramDict = paramDict)

class AwlSimMessage_SET_OPT(AwlSimMessage):
	def __init__(self, name, value):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_SET_OPT)
		self.name = name
		self.value = value

	def getStrValue(self):
		return self.value

	def getIntValue(self):
		try:
			return int(self.value)
		except ValueError as e:
			raise AwlSimError("SET_OPT: Value is not an integer")

	def getBoolValue(self):
		try:
			return bool(self.getIntValue())
		except ValueError as e:
			raise AwlSimError("SET_OPT: Value is not a boolean")

	def getFloatValue(self):
		try:
			return float(self.value)
		except ValueError as e:
			raise AwlSimError("SET_OPT: Value is not a float")

	def toBytes(self):
		try:
			payload = self.packString(self.name)
			payload += self.packString(self.value)
		except ValueError as e:
			raise TransferError("SET_OPT: Invalid data format")
		return AwlSimMessage.toBytes(self, len(payload)) + payload

	@classmethod
	def fromBytes(cls, payload):
		try:
			offset = 0
			name, count = cls.unpackString(payload, offset)
			offset += count
			value, count = cls.unpackString(payload, offset)
			offset += count
		except ValueError as e:
			raise TransferError("SET_OPT: Invalid data format")
		return cls(name = name, value = value)

class AwlSimMessage_CPUDUMP(AwlSimMessage):
	def __init__(self, dumpText):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_CPUDUMP)
		self.dumpText = dumpText

	def toBytes(self):
		try:
			dumpBytes = self.dumpText.encode()
			return AwlSimMessage.toBytes(self, len(dumpBytes)) + dumpBytes
		except UnicodeError:
			raise TransferError("CPUDUMP: Unicode error")

	@classmethod
	def fromBytes(cls, payload):
		try:
			dumpText = payload.decode()
		except UnicodeError:
			raise TransferError("CPUDUMP: Unicode error")
		return cls(dumpText)

class AwlSimMessage_MAINTREQ(AwlSimMessage):
	plStruct = struct.Struct(str(">H"))

	def __init__(self, requestType):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_MAINTREQ)
		self.requestType = requestType

	def toBytes(self):
		pl = self.plStruct.pack(self.requestType)
		return AwlSimMessage.toBytes(self, len(pl)) + pl

	@classmethod
	def fromBytes(cls, payload):
		try:
			(requestType, ) = cls.plStruct.unpack(payload)
		except struct.error as e:
			raise TransferError("MAINTREQ: Invalid data format")
		return cls(requestType)

class AwlSimMessage_GET_CPUSPECS(AwlSimMessage):
	def __init__(self):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_GET_CPUSPECS)

class AwlSimMessage_CPUSPECS(AwlSimMessage):
	plStruct = struct.Struct(str(">32I"))

	def __init__(self, cpuspecs):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_CPUSPECS)
		self.cpuspecs = cpuspecs

	def toBytes(self):
		pl = self.plStruct.pack(self.cpuspecs.getConfiguredMnemonics(),
					self.cpuspecs.nrAccus,
					self.cpuspecs.nrTimers,
					self.cpuspecs.nrCounters,
					self.cpuspecs.nrFlags,
					self.cpuspecs.nrInputs,
					self.cpuspecs.nrOutputs,
					self.cpuspecs.nrLocalbytes,
					*( (0,) * 24 ) # padding
		)
		return AwlSimMessage.toBytes(self, len(pl)) + pl

	@classmethod
	def fromBytes(cls, payload):
		try:
			data = cls.plStruct.unpack(payload)
			(mnemonics, nrAccus, nrTimers,
			 nrCounters, nrFlags, nrInputs,
			 nrOutputs, nrLocalbytes) = data[:8]
		except struct.error as e:
			raise TransferError("CPUSPECS: Invalid data format")
		cpuspecs = S7CPUSpecs()
		cpuspecs.setConfiguredMnemonics(mnemonics)
		cpuspecs.setNrAccus(nrAccus)
		cpuspecs.setNrTimers(nrTimers)
		cpuspecs.setNrCounters(nrCounters)
		cpuspecs.setNrFlags(nrFlags)
		cpuspecs.setNrInputs(nrInputs)
		cpuspecs.setNrOutputs(nrOutputs)
		cpuspecs.setNrLocalbytes(nrLocalbytes)
		return cls(cpuspecs)

class AwlSimMessage_REQ_MEMORY(AwlSimMessage):
	# Payload header struct:
	#	flags (32 bit)
	#	repetition factor (32 bit)
	plHdrStruct = struct.Struct(str(">II"))

	# Payload memory area struct:
	#	memType (8 bit)
	#	flags (8 bit)
	#	index (16 bit)
	#	start (32 bit)
	#	length (32 bit)
	plAreaStruct = struct.Struct(str(">BBHII"))

	# Flags
	FLG_SYNC	= 1 << 0 # Synchronous. Returns a REPLY when finished.

	def __init__(self, flags, repetitionFactor, memAreas):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_REQ_MEMORY)
		self.flags = flags
		self.repetitionFactor = repetitionFactor
		self.memAreas = memAreas

	def toBytes(self):
		pl = self.plHdrStruct.pack(self.flags,
					   self.repetitionFactor)
		for memArea in self.memAreas:
			pl += self.plAreaStruct.pack(memArea.memType,
						     memArea.flags,
						     memArea.index,
						     memArea.start,
						     memArea.length)
		return AwlSimMessage.toBytes(self, len(pl)) + pl

	@classmethod
	def fromBytes(cls, payload):
		try:
			offset = 0
			flags, repetitionFactor =\
				cls.plHdrStruct.unpack_from(payload, offset)
			offset += cls.plHdrStruct.size
			memAreas = []
			while offset < len(payload):
				memType, mFlags, index, start, length =\
					cls.plAreaStruct.unpack_from(payload, offset)
				offset += cls.plAreaStruct.size
				memAreas.append(MemoryArea(memType, mFlags, index, start, length))
		except struct.error as e:
			raise TransferError("REQ_MEMORY: Invalid data format")
		return cls(flags, repetitionFactor, memAreas)

class AwlSimMessage_MEMORY(AwlSimMessage):
	# Payload header struct:
	#	flags (32 bit)
	plHdrStruct = struct.Struct(str(">I"))

	# Payload memory area struct:
	#	memType (8 bit)
	#	flags (8 bit)
	#	index (16 bit)
	#	start (32 bit)
	#	specified length (32 bit)
	#	actual length (32 bit)
	#	the actual binary data (variable length, padded to 32-bit boundary)
	plAreaStruct = struct.Struct(str(">BBHIII"))

	# Flags
	FLG_SYNC	= 1 << 0 # Synchronous. Returns a REPLY when finished.

	def __init__(self, flags, memAreas):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_MEMORY)
		self.flags = flags
		self.memAreas = memAreas

	def toBytes(self):
		pl = [ self.plHdrStruct.pack(self.flags) ]
		for memArea in self.memAreas:
			actualLength = len(memArea.data)
			pl.append(self.plAreaStruct.pack(memArea.memType,
							 memArea.flags,
							 memArea.index,
							 memArea.start,
							 memArea.length,
							 actualLength))
			pl.append(memArea.data)
			# Pad to a 32-bit boundary
			pl.append(b'\x00' * (round_up(actualLength, 4) - actualLength))
		pl = b''.join(pl)
		return AwlSimMessage.toBytes(self, len(pl)) + pl

	@classmethod
	def fromBytes(cls, payload):
		try:
			offset = 0
			(flags, ) = cls.plHdrStruct.unpack_from(payload, offset)
			offset += cls.plHdrStruct.size
			memAreas = []
			while offset < len(payload):
				memType, mFlags, index, start, length, actualLength =\
					cls.plAreaStruct.unpack_from(payload, offset)
				offset += cls.plAreaStruct.size
				data = payload[offset : offset + actualLength]
				offset += round_up(actualLength, 4)
				if len(data) != actualLength:
					raise IndexError
				memAreas.append(MemoryArea(memType, mFlags, index,
							   start, length, data))
		except (struct.error, IndexError) as e:
			raise TransferError("MEMORY: Invalid data format")
		return cls(flags, memAreas)

class AwlSimMessage_INSNSTATE(AwlSimMessage):
	# Payload data struct:
	#	AWL line number (32 bit)
	#	Serial number. Reset to 0 on cycle exit. (32 bit)
	#	Flags (16 bit)
	#	CPU status word (16 bit)
	#	CPU ACCU 1 (32 bit)
	#	CPU ACCU 2 (32 bit)
	#	CPU AR 1 (32 bit)
	#	CPU AR 2 (32 bit)
	#	CPU DB register (16 bit)
	#	CPU DI register (16 bit)
	plDataStruct = struct.Struct(">IIHHIIIIHH")

	def __init__(self, lineNr, serial, flags, stw, accu1, accu2, ar1, ar2, db, di):
		AwlSimMessage.__init__(self, AwlSimMessage.MSG_ID_INSNSTATE)
		self.lineNr = lineNr
		self.serial = serial
		self.flags = flags
		self.stw = stw
		self.accu1 = accu1
		self.accu2 = accu2
		self.ar1 = ar1
		self.ar2 = ar2
		self.db = db
		self.di = di

	def toBytes(self):
		pl = self.plDataStruct.pack(self.lineNr, self.serial,
			self.flags, self.stw, self.accu1, self.accu2,
			self.ar1, self.ar2, self.db, self.di)
		return AwlSimMessage.toBytes(self, len(pl)) + pl

	@classmethod
	def fromBytes(cls, payload):
		try:
			lineNr, serial, flags, stw, accu1, accu2, ar1, ar2, db, di =\
				cls.plDataStruct.unpack_from(payload, 0)
		except (struct.error, IndexError) as e:
			raise TransferError("INSNSTATE: Invalid data format")
		return cls(lineNr, serial, flags, stw, accu1, accu2, ar1, ar2, db, di)

class AwlSimMessageTransceiver(object):
	class RemoteEndDied(Exception): pass

	id2class = {
		AwlSimMessage.MSG_ID_REPLY		: AwlSimMessage_REPLY,
		AwlSimMessage.MSG_ID_EXCEPTION		: AwlSimMessage_EXCEPTION,
		AwlSimMessage.MSG_ID_PING		: AwlSimMessage_PING,
		AwlSimMessage.MSG_ID_PONG		: AwlSimMessage_PONG,
		AwlSimMessage.MSG_ID_RESET		: AwlSimMessage_RESET,
		AwlSimMessage.MSG_ID_RUNSTATE		: AwlSimMessage_RUNSTATE,
		AwlSimMessage.MSG_ID_LOAD_SYMTAB	: AwlSimMessage_LOAD_SYMTAB,
		AwlSimMessage.MSG_ID_LOAD_CODE		: AwlSimMessage_LOAD_CODE,
		AwlSimMessage.MSG_ID_LOAD_HW		: AwlSimMessage_LOAD_HW,
		AwlSimMessage.MSG_ID_SET_OPT		: AwlSimMessage_SET_OPT,
		AwlSimMessage.MSG_ID_CPUDUMP		: AwlSimMessage_CPUDUMP,
		AwlSimMessage.MSG_ID_MAINTREQ		: AwlSimMessage_MAINTREQ,
		AwlSimMessage.MSG_ID_GET_CPUSPECS	: AwlSimMessage_GET_CPUSPECS,
		AwlSimMessage.MSG_ID_CPUSPECS		: AwlSimMessage_CPUSPECS,
		AwlSimMessage.MSG_ID_REQ_MEMORY		: AwlSimMessage_REQ_MEMORY,
		AwlSimMessage.MSG_ID_MEMORY		: AwlSimMessage_MEMORY,
		AwlSimMessage.MSG_ID_INSNSTATE		: AwlSimMessage_INSNSTATE,
	}

	def __init__(self, sock, peerInfoString):
		self.sock = sock
		self.peerInfoString = peerInfoString

		# Transmit status
		self.txSeqCount = 0

		# Receive buffer
		self.buf = b""
		self.msgId = None
		self.seq = None
		self.payloadLen = None

		try:
			self.__timeout = None
			self.sock.settimeout(self.__timeout)

			if self.sock.family in (socket.AF_INET, socket.AF_INET6) and\
			   self.sock.type == socket.SOCK_STREAM:
				self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
			self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 2048)
			self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 8192)
		except socket.error as e:
			raise AwlSimError("Failed to initialize socket: %s" % str(e))

	def shutdown(self):
		if self.sock:
			try:
				self.sock.shutdown(socket.SHUT_RDWR)
			except socket.error as e:
				pass
			try:
				self.sock.close()
			except socket.error as e:
				pass
			self.sock = None

	def send(self, msg, timeout=None):
		if timeout != self.__timeout:
			self.sock.settimeout(timeout)
			self.__timeout = timeout

		msg.seq = self.txSeqCount
		self.txSeqCount = (self.txSeqCount + 1) & 0xFFFF

		offset, data = 0, msg.toBytes()
		while offset < len(data):
			try:
				offset += self.sock.send(data[offset : ])
			except (socket.error, BlockingIOError) as e:
				if e.errno != errno.EAGAIN and\
				   e.errno != errno.EWOULDBLOCK and\
				   not isinstance(e, BlockingIOError) and\
				   not isinstance(e, socket.timeout):
					raise TransferError(str(e))

	def receive(self, timeout=0.0):
		if timeout != self.__timeout:
			self.sock.settimeout(timeout)
			self.__timeout = timeout

		hdrLen = AwlSimMessage.HDR_LENGTH
		if len(self.buf) < hdrLen:
			try:
				data = self.sock.recv(hdrLen - len(self.buf))
			except (socket.error, BlockingIOError) as e:
				if e.errno == errno.EAGAIN or\
				   e.errno == errno.EWOULDBLOCK or\
				   isinstance(e, BlockingIOError):
					return None
				raise
			if not data:
				# The remote end closed the connection
				raise self.RemoteEndDied()
			self.buf += data
			if len(self.buf) < hdrLen:
				return None
			try:
				magic, self.msgId, self.seq, _reserved, self.payloadLen =\
					AwlSimMessage.hdrStruct.unpack(self.buf)
			except struct.error as e:
				raise AwlSimError("Received message with invalid "
					"header format.")
			if magic != AwlSimMessage.HDR_MAGIC:
				raise AwlSimError("Received message with invalid "
					"magic value (was 0x%04X, expected 0x%04X)." %\
					(magic, AwlSimMessage.HDR_MAGIC))
			if self.payloadLen:
				return None
		if len(self.buf) < hdrLen + self.payloadLen:
			data = self.sock.recv(hdrLen + self.payloadLen - len(self.buf))
			if not data:
				# The remote end closed the connection
				raise self.RemoteEndDied()
			self.buf += data
			if len(self.buf) < hdrLen + self.payloadLen:
				return None
		try:
			cls = self.id2class[self.msgId]
		except KeyError:
			raise AwlSimError("Received unknown message: 0x%04X" %\
				self.msgId)
		msg = cls.fromBytes(self.buf[hdrLen : ])
		msg.seq = self.seq
		self.buf, self.msgId, self.seq, self.payloadLen = b"", None, None, None
		return msg
